<?
/**
 * Copyright 2007 Melange.
 *
 * This file is part of PHP-AAS.
 *
 * PHP-AAS is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * PHP-AAS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PHP-AAS; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @category    Melange
 * @package     php-aas
 * @subpackage  realm
 * @copyright   Copyright (c) 2007 Jeroen Simons. All rights reserved
 * @author      Jeroen Simons <jeroen@melange.nl>
 * @link        http://www.melange.nl/
 *
 */

require_once("aas/realm/RealmBase.php");


/**
 * Implmentation of <b>Realm</b> that works with any JDBC supported database.
 * See the JDBCRealm->howto for more details on how to set up the database and
 * for configuration options.
 * <p/>
 * <p><strong>TODO</strong> - Support connection pooling ($including message
 * format objects) so that <code>authenticate()</code>,
 * <code>getPassword()</code> and <code>authenticate()</code> do not have to be
 * synchronized and would fix the ugly connection logic. </p>
 *
 * @author Craig R. McClanahan
 * @author Carson McDonald
 * @author Ignacio Ortega
 * @version $Revision: 476979 $ $Date: 2006-11-20 00:52:52 +0100 ($lun., 20 nov. 2006) $
 */

class MySQLRealm
        extends RealmBase {


    /**
     * Log
     */
    protected static $log = null;


    // ----------------------------------------------------- Instance Variables


    /**
     * The connection username to use when trying to connect to the database.
     */
    protected $connectionName = null;


    /**
     * The connection URL to use when trying to connect to the database.
     */
    protected $connectionPassword = null;


    /**
     * The connection URL to use when trying to connect to the database.
     */
    protected $connectionURL = null;


    /**
     * The connection to the database.
     */
    protected $dbConnection = null;


    /**
     * Instance of the JDBC Driver class we use as a connection factory.
     */
    protected $driver = null;


    /**
     * The JDBC driver to use.
     */
    protected $driverName = null;


    protected $name = null;


    /**
     * The PreparedStatement to use for authenticating users.
     */
    protected $preparedCredentials = null;


    /**
     * The PreparedStatement to use for identifying the roles for
     * a specified user.
     */
    protected $preparedRoles = null;


    /**
     * The column in the user role table that names a role
     */
    protected $roleNameCol = null;


    /**
     * The column in the user table that holds the user's credintials
     */
    protected $userCredCol = null;


    /**
     * The column in the user table that holds the user's name
     */
    protected $userNameCol = null;


    /**
     * The table that holds the relation between user's and roles
     */
    protected $userRoleTable = null;


    /**
     * The table that holds user data.
     */
    protected $userTable = null;


    /**
     * The table that holds user data.
     */
    protected $where = null;



    // ------------------------------------------------------------- Properties


    public function __construct() {

        parent::__construct();
        $this->log = LoggerManager::getLogger("MySQLRealm");

    }


    // ------------------------------------------------------------- Properties

    /**
     * Return the username to use to connect to the database.
     */
    public function getConnectionName() {
        return $this->connectionName;
    }

    /**
     * Set the username to use to connect to the database.
     *
     * @param connectionName Username
     */
    public function setConnectionName($connectionName) {
        $this->connectionName = $connectionName;
    }

    /**
     * Return the password to use to connect to the database.
     */
    public function getConnectionPassword() {
        return $this->connectionPassword;
    }

    /**
     * Set the password to use to connect to the database.
     *
     * @param connectionPassword User password
     */
    public function setConnectionPassword($connectionPassword) {
        $this->connectionPassword = $connectionPassword;
    }

    /**
     * Return the URL to use to connect to the database.
     */
    public function getConnectionURL() {
        return $this->connectionURL;
    }

    /**
     * Set the URL to use to connect to the database.
     *
     * @param connectionURL The new connection URL
     */
    public function setConnectionURL($connectionURL) {
        $this->connectionURL = $connectionURL;
    }

    /**
     * Return the column in the user role table that names a role.
     */
    public function getRoleNameCol() {
        return $this->roleNameCol;
    }

    /**
     * Set the column in the user role table that names a role.
     *
     * @param roleNameCol The column name
     */
    public function setRoleNameCol($roleNameCol) {
        $this->roleNameCol = $roleNameCol;
    }

    /**
     * Return the column in the user table that holds the user's credentials.
     */
    public function getUserCredCol() {
        return $this->userCredCol;
    }

    /**
     * Set the column in the user table that holds the user's credentials.
     *
     * @param userCredCol The column name
     */
    public function setUserCredCol($userCredCol) {
        $this->userCredCol = $userCredCol;
    }

    /**
     * Return the column in the user table that holds the user's name.
     */
    public function getUserNameCol() {
        return $this->userNameCol;
    }

    /**
     * Set the column in the user table that holds the user's name.
     *
     * @param userNameCol The column name
     */
    public function setUserNameCol($userNameCol) {
        $this->userNameCol = $userNameCol;
    }

    /**
     * Return the table that holds the relation between user's and roles.
     */
    public function getUserRoleTable() {
        return $this->userRoleTable;
    }

    /**
     * Set the table that holds the relation between user's and roles.
     *
     * @param userRoleTable The table name
     */
    public function setUserRoleTable($userRoleTable) {
        $this->userRoleTable = $userRoleTable;
    }

    /**
     * Return the table that holds user data..
     */
    public function getUserTable() {
        return $this->userTable;
    }

    /**
     * Set the table that holds user data.
     *
     * @param userTable The table name
     */
    public function setUserTable($userTable) {
        $this->userTable = $userTable;
    }

    /**
     * Return the 'where' clause add on..
     */
    public function getWhereClause() {
        return $this->where;
    }

    /**
     * Return the 'where' clause add on.
     *
     * @param where The where clause
     */
    public function setWhereClause($where) {
        $this->where = $where;
    }

    // --------------------------------------------------------- Public Methods


    /**
     * Return the Principal associated with the specified username and
     * credentials, $if there is one; otherwise return <code>null</code>.
     * <p/>
     * If there are any errors with the JDBC connection, $executing
     * the query or anything we return null ($don't authenticate). This
     * event is also logged, $and the connection will be closed so that
     * a subsequent request will automatically re-open it.
     *
     * @param username    Username of the Principal to look up
     * @param credentials Password or other credentials to use in
     *                    authenticating this username
     */
    public function authenticate($username, $credentials) {

        // Number of tries is the numebr of attempts to connect to the database
        // during this login attempt ($if we need to open the database)
        // This needs rewritten wuth better pooling support, $the existing code
        // needs signature changes since the Prepared statements needs cached
        // with the connections.
        // The code below will try twice if there is a SQLException so the
        // connection may try to be opened again. On normal conditions ($including
        // invalid login - the above is only used once.
        $numberOfTries = 2;
        while ($numberOfTries > 0) {

            try {

                // Ensure that we have an open database connection
                $this->open();

                // Acquire a Principal object for this user
                $principal = $this->authenticateDB($this->dbConnection,
                        $username, $credentials);

                // Return the Principal ($if any)
                return ($principal);

            } catch (Exception $e) {

                // Log the problem for posterity
                $this->log->error("Exception performing authentication " . $e);

                // Close the connection so that it gets reopened next time
                if ($this->dbConnection !== null)
                    $this->close($this->dbConnection);

            }

            $numberOfTries--;
        }

        // Worst case scenario
        return null;

    }

    // -------------------------------------------------------- Package Methods

    // ------------------------------------------------------ Protected Methods


    /**
     * Return the Principal associated with the specified username and
     * credentials, $if there is one; otherwise return <code>null</code>.
     *
     * @param dbConnection The database connection to be used
     * @param username     Username of the Principal to look up
     * @param credentials  Password or other credentials to use in
     *                     authenticating this username
     */
    public function authenticateDB($dbConnection,
                                          $username,
                                          $credentials) {

        // No user - can't possibly authenticate
        if ($username === null) {
            return (null);
        }

        // Look up the user's credentials
        $dbCredentials = $this->getPassword($username);

        // Validate the user's credentials
        $validated = false;
        if ($this->hasMessageDigest()) {
            // Hex hashes should be compared case-insensitive
            $validated = (strtolower($this->digest($credentials)) === strtolower($dbCredentials));
        } else {
            $validated = ($this->digest($credentials) === $dbCredentials);
        }

        if ($validated) {
            if ($this->log->isDebugEnabled())
                $this->log->debug("Username ${username} successfully authenticated");
        } else {
            if ($this->log->isDebugEnabled())
                $this->log->debug("Username ${username} NOT successfully authenticated");
            return (null);
        }

       $roles = $this->getRoles($username);

        // Create and return a suitable Principal for this user
        return (new GenericPrincipal($this, $username, $credentials, $roles));

    }


    /**
     * Close the specified database connection.
     *
     * @param dbConnection The connection to be closed
     */
    protected function close($dbConnection) {

        mysql_close($dbConnection);

    }


    /**
     * Return a PreparedStatement configured to perform the SELECT required
     * to retrieve user credentials for the specified username.
     *
     * @param dbConnection The database connection to be used
     * @param username     Username for which credentials should be retrieved
     * @throws SQLException if a database error occurs
     */
    protected function credentials($username) {

        if ($this->preparedCredentials === null) {
            $sb = "SELECT ";
            $sb .= $this->userCredCol;
            $sb .= " FROM ";
            $sb .= $this->userTable;
            $sb .= " WHERE ";
            $sb .= $this->userNameCol;
            $sb .= " = ?";

            if($this->where !== null)
                $sb .= " AND ".$this->where;

            if ($this->log->isDebugEnabled()) {
                $this->log->debug("credentials query: " . $sb);
            }

            $this->preparedCredentials = $sb;
        }

        if ($username === null) {
            $this->preparedCredentials = str_replace("?", "NULL", $this->preparedCredentials);
        } else {
            $this->preparedCredentials = str_replace("?", "'" . mysql_real_escape_string($username). "'", $this->preparedCredentials);
        }

        return ($this->preparedCredentials);
    }


    /**
     * Return a short name for this Realm implementation.
     */
    protected function getName() {

        return ($this->name);

    }


    /**
     * Return the password associated with the given principal's user name.
     */
    protected function getPassword($username) {

        // Look up the user's credentials
        $dbCredentials = null;
        $stmt = null;
        $rs = null;

        // Number of tries is the numebr of attempts to connect to the database
        // during this login attempt ($if we need to open the database)
        // This needs rewritten wuth better pooling support, $the existing code
        // needs signature changes since the Prepared statements needs cached
        // with the connections.
        // The code below will try twice if there is a SQLException so the
        // connection may try to be opened again. On normal conditions ($including
        // invalid login - the above is only used once.
        $numberOfTries = 2;
        while ($numberOfTries > 0) {

            try {

                // Ensure that we have an open database connection
                $this->open();

                $stmt = $this->credentials($username);
                $rs = mysql_query($stmt, $this->dbConnection);

                $row = mysql_fetch_row($rs);

                $dbCredentials = $row[0];

                if ($dbCredentials === null) {
                    return (null);
                }

                $dbCredentials = trim($dbCredentials);

                return $dbCredentials;

            } catch (SQLException $e) {

                // Log the problem for posterity
                $this->log->error($sm->getString("jdbcRealm->exception"), e);

                // Close the connection so that it gets reopened next time
                if ($this->dbConnection !== null)
                    $this->close($dbConnection);

            }

            $numberOfTries--;
        }

        return (null);
    }


    /**
     * Return the Principal associated with the given user name.
     */
    protected function getPrincipal($username) {

        return (new GenericPrincipal($this,
                $username,
                $this->getPassword($username),
                $this->getRoles($username)));

    }


    /**
     * Return the roles associated with the gven user name.
     */
    protected function getRoles($username) {

        $stmt = null;
        $rs = null;

        // Number of tries is the numebr of attempts to connect to the database
        // during this login attempt ($if we need to open the database)
        // This needs rewritten wuth better pooling support, $the existing code
        // needs signature changes since the Prepared statements needs cached
        // with the connections.
        // The code below will try twice if there is a SQLException so the
        // connection may try to be opened again. On normal conditions ($including
        // invalid login - the above is only used once.
        $numberOfTries = 2;
        while ($numberOfTries > 0) {
            try {

                // Ensure that we have an open database connection
                $this->open();

                // Accumulate the user's roles
                $roleList = array();
                $stmt = $this->roles($this->dbConnection, $username);
                $rs = mysql_query($stmt, $this->dbConnection);

                while ($row = mysql_fetch_array($rs, MYSQL_NUM)) {
                    $role = array_key_exists(0, $row) ? $row[0] : null;
                    if (null !== $role) {
                        $roleList[] = trim($role);
                    }
                }

                return ($roleList);

            } catch (SQLException $e) {

                // Log the problem for posterity
                $this->log->error("Exception performing authentication " . $e);

                // Close the connection so that it gets reopened next time
                if ($this->dbConnection !== null)
                    $this->close($this->dbConnection);

            }

            $numberOfTries--;
        }

        return (null);

    }


    /**
     * Open ($if necessary) and return a database connection for use by
     * this Realm.
     *
     * @throws SQLException if a database error occurs
     */
    protected function open() {

        // Do nothing if there is a database connection already open
        if ($this->dbConnection !== null)
            return ($this->dbConnection);

        $slash = strpos($this->connectionURL, "/");
        $host  = substr($this->connectionURL, 0, $slash);
        $db    = substr($this->connectionURL, $slash + 1);

        $this->dbConnection = mysql_connect($host, $this->connectionName, $this->connectionPassword);
        if (!$this->dbConnection) {
           $this->log->error("Not connected : " . mysql_error());
           throw new Exception(mysql_error());
        }

        $selected = mysql_select_db($db, $this->dbConnection);
        if (!$selected) {
           $this->log->error("Can't use ${db} : " . mysql_error());
           throw new Exception(mysql_error());
        }

        return ($this->dbConnection);

    }


    /**
     * Release our use of this connection so that it can be recycled.
     *
     * @param dbConnection The connection to be released
     */
    protected function release(Connection $dbConnection) {

        ; // NO-OP since we are not pooling anything

    }


    /**
     * Return a PreparedStatement configured to perform the SELECT required
     * to retrieve user roles for the specified username.
     *
     * @param dbConnection The database connection to be used
     * @param username     Username for which roles should be retrieved
     * @throws SQLException if a database error occurs
     */
    protected function roles($dbConnection, $username) {

        if ($this->preparedRoles === null) {
            $sb = "SELECT ";
            $sb .= $this->roleNameCol;
            $sb .= " FROM ";
            $sb .= $this->userRoleTable;
            $sb .= " WHERE ";
            $sb .= $this->userNameCol;
            $sb .= " = ?";
            $this->preparedRoles = $sb;
        }

        $this->preparedRoles = str_replace("?",
            "'" . mysql_real_escape_string($username) . "'",
            $this->preparedRoles);

        return ($this->preparedRoles);

    }

    public function __clone() {
        $this->log = null;
        $this->connectionName = null;
        $this->connectionPassword = null;
        $this->connectionURL = null;
        $this->dbConnection = null;
        $this->driver = null;
        $this->driverName = null;
        $this->name = null;
        $this->preparedCredentials = null;
        $this->preparedRoles = null;
        $this->roleNameCol = null;
        $this->userCredCol = null;
        $this->userNameCol = null;
        $this->userRoleTable = null;
        $this->userTable = null;
        $this->where = null;
        $this->applicationContext = null;
        $this->container = null;
        $this->started = null;
        $this->md = null;
        $this->digest = null;
    }

}
?>
